﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Behavioral.Private;

namespace Behavioral
{
    namespace Private
    {
        public abstract class UnitTestBase<TTarget> : IUnitTest, IContextSetter, IContextClearerInternal
        {
            #region Methods

            protected UnitTestBase()
            {
                this.tearDowns = new List<ITearDownMarker>();
                this.errorHandlers = new List<IErrorHandlerMarker>();
                this.initializers = new List<IInitializerMarker>();

                this.initializationRunner = new InitializationRunner();
                this.actionRunner = new ActionRunner();

                this.target = default(TTarget);

                OutputFriendly(this.GetType(), null, @"'", @"'", true);
            }

            protected void StoreTearDown<TInitializer>(TInitializer initializer)
                where TInitializer : IInitializerMarker
            {
                ITearDownMarker tearDown = initializer as ITearDownMarker;
                if (tearDown != null)
                {
                    this.tearDowns.Add(tearDown);
                }
            }

            protected void StoreErrorHandler<TInitializer>(TInitializer initializer)
                where TInitializer : IInitializerMarker
            {
                IErrorHandlerMarker errorHandler = initializer as IErrorHandlerMarker;
                if (errorHandler != null)
                {
                    this.errorHandlers.Add(errorHandler);
                }
            }
        
            internal void GivenThat(Type initializerType, params object[] constructorParameters)
            {
                OutputFriendly(initializerType, constructorParameters, "Given that ");
                this.ClearContext();                
                this.SetContext<IUnitTest>(this);
                this.SetContext(this.initializationRunner);
                Initialize(initializerType, constructorParameters);
            }

            protected void Initialize(Type initializerType, params object[] constructorParameters)
            {
                var initializer =
                    Activator.CreateInstance(initializerType, constructorParameters) as IInitializerMarker;
                StoreTearDown(initializer);
                StoreErrorHandler(initializer);
                this.initializers.Add(initializer);
            }

            protected void ExecuteActionWithoutExpectedExceptions<TAction>(TAction action)
                where TAction : IActionable
            {
                int placeholderReturnValue = 0;
                this.actionRunner.Execute(action, ref this.target, ref placeholderReturnValue);
            }

            protected void ExecuteActionAndExpectException<TAction>(TAction action, Type expectedExceptionType)
                where TAction : IActionable
            {
                bool exceptionWasThrown = false;
                try
                {
                    ExecuteActionWithoutExpectedExceptions(action);
                }
                catch (Exception thrownException)
                {
                    exceptionWasThrown = true;
                    bool unexpectedExceptionWasThrown = !expectedExceptionType.IsAssignableFrom(thrownException.GetType());
                    if (unexpectedExceptionWasThrown)
                    {
                        throw;
                    }
                }
                finally
                {
                    TearDown();   
                }
                if (!exceptionWasThrown)
                {
                    throw new Exception(string.Format("Expected '{0}' but not thrown", expectedExceptionType.Name));
                }
            }

            protected TReturnValue ExecuteActionWithoutExpectedExceptions<TAction, TReturnValue>(TAction action)
                where TAction : IActionable
            {
                TReturnValue returnValue = default(TReturnValue);
                this.actionRunner.Execute(action, ref this.target, ref returnValue);
                return returnValue;
            }

            protected TReturnValue ExecuteActionAndExpectException<TAction, TReturnValue>(TAction action, Type expectedExceptionType)
                where TAction : IActionable
            {
                TReturnValue returnValue = default(TReturnValue);
                bool exceptionWasThrown = false;
                try
                {
                    returnValue = ExecuteActionWithoutExpectedExceptions<IActionable, TReturnValue>(action);
                }
                catch (Exception thrownException)
                {
                    exceptionWasThrown = true;
                    bool unexpectedExceptionWasThrown = !expectedExceptionType.IsAssignableFrom(thrownException.GetType());
                    if (unexpectedExceptionWasThrown)
                    {
                        throw;
                    }
                }
                finally
                {
                    TearDown();
                }
                if (!exceptionWasThrown)
                {
                    throw new Exception(string.Format("Expected '{0}' but not thrown", expectedExceptionType.Name));
                }
                return returnValue;
            }

            protected void RunInitializers()
            {
                foreach (IInitializerMarker initializer in this.initializers)
                {
                    this.initializationRunner.SetUp(initializer, ref this.target);
                    RunActionAsInitializer(initializer as IActionable);
                }
            }

            #endregion

            #region IUnitTest Implementation

            [Obsolete("The Run method is no longer considered part of the UnitTest contract and will be removed in future versions (thus breaking your 'override'). Feel free to choose a more descriptive name for your test method.")]
            public virtual void Run()
            {} 

            public virtual void TearDown()
            {
                foreach (ITearDownMarker tearDown in this.tearDowns)
                {
                    this.initializationRunner.TearDown(tearDown, this.target);
                }
            }

            protected abstract void RunActionAsInitializer(IActionable action);

            #endregion

            #region String output

            protected void OutputFriendly(Type initializerType, object[] parameters, string prefix = null, string suffix = null, bool initialCapital = false)
            {
                string typeName = initializerType.Name;
                string typeAsNaturalLanguage = ConvertPascalCaseToNaturalLanguage(typeName);

                if (string.IsNullOrEmpty(prefix))
                {
                    typeAsNaturalLanguage = typeAsNaturalLanguage.Substring(1);
                }

                if(initialCapital && !string.IsNullOrEmpty(typeAsNaturalLanguage))
                {
                    typeAsNaturalLanguage = typeAsNaturalLanguage.First().ToString().ToUpper() +
                                            typeAsNaturalLanguage.Substring(1);
                }

                string parameterString = string.Empty;
                if (parameters != null && parameters.Length > 0)
                {
                    List<string> paramStrings = new List<string>();
                    parameters.ToList().ForEach(param => paramStrings.Add(param == null ? "{null}" : param is string ? string.Format("\"{0}\"", param) : param.ToString()));
                    parameterString = string.Join(", ", paramStrings.ToArray());
                    parameterString = string.Format(" [{0}]", parameterString);
                }

                Console.WriteLine("{0}{1}{2}{3}", prefix, typeAsNaturalLanguage, parameterString, suffix);
            }

            protected void OutputTestMethodName()
            {
                var stack = new StackTrace();
                StackFrame clientCodeStackFrame = null;
                int currentFrameIndex = 0;
                while (clientCodeStackFrame == null)
                {
                    bool stackFrameIsFromClientCode = (stack.GetFrame(currentFrameIndex).GetMethod().Module.Assembly != Assembly.GetCallingAssembly());
                    Console.WriteLine("StackMethod:{0}", stack.GetFrame(currentFrameIndex).GetMethod().Name);
                    if (stackFrameIsFromClientCode)
                    {
                        clientCodeStackFrame = stack.GetFrame(currentFrameIndex);
                    }
                    ++currentFrameIndex;
                }
                string friendlyMethodName = ConvertPascalCaseToNaturalLanguage(clientCodeStackFrame.GetMethod().Name);
                friendlyMethodName = friendlyMethodName.First().ToString().ToUpper() +
                                            friendlyMethodName.Substring(1);
                Console.WriteLine("{0}:", friendlyMethodName);
            }

            protected string ConvertPascalCaseToNaturalLanguage(string pascalCase)
            {
                Regex regex = new Regex("[A-Z]");
                string sentence = regex.Replace(pascalCase, " $&");
                string naturalLanguage = sentence.ToLower();
                return naturalLanguage.Substring(1);
            }

            #endregion

            #region Properties

            object IUnitTest.Target
            { 
                get
                {
                    return this.target;
                }
                set
                {
                    this.target = (TTarget)value;
                }
            }

            #endregion

            #region Fields

            private ICollection<ITearDownMarker> tearDowns;
            private ICollection<IErrorHandlerMarker> errorHandlers;
            private ICollection<IInitializerMarker> initializers;

            private InitializationRunner initializationRunner;
            private ActionRunner actionRunner;

            protected Type expectedException;

            protected Exception exceptionThrown;

            protected TTarget target;

            protected IActionable actionInstance;

            #endregion
        }

        public delegate TResult Function<in T, out TResult>(T parameter);
    }

    public abstract class UnitTest<TTarget> : UnitTestBase<TTarget>, IInitializationFluent<TTarget>, IExpectationFluent<TTarget>, IFurtherExpectationFluent<TTarget>
    {
        #region Methods

        IFurtherExpectationFluent<TTarget> IExpectationFluent<TTarget>.Then<TAssertion>(params object[] constructorParameters)
        {
            // We don't expect action to throw
            ExecuteActionWithoutExpectedExceptions(this.actionInstance);
            OutputFriendly(typeof(TAssertion), constructorParameters, "then ");
            return Result<TAssertion>(constructorParameters);
        }

        IFurtherExpectationFluent<TTarget> Result<TAssertion>(params object[] constructorParameters)
        {
            var assertion = Activator.CreateInstance(typeof(TAssertion), constructorParameters) as IAssertion<TTarget>;
            assertion.Verify(this.target);
            return this;
        }

        IFurtherExpectationFluent<TTarget> IFurtherExpectationFluent<TTarget>.And<TAssertion>(params object[] constructorParameters)
        {
            OutputFriendly(typeof(TAssertion), constructorParameters, "\tand ");
            return Result<TAssertion>(constructorParameters);
        }

        #endregion

        #region IUnitTest<TTarget> Member

        protected IInitializationFluent<TTarget> GivenThat<TInitializer>(params object[] constructorParameters)
                where TInitializer : IInitializerMarker
        {
            GivenThat(typeof(TInitializer), constructorParameters);
            return this;
        }

        IInitializationFluent<TTarget> IInitializationFluent<TTarget>.And<TInitializer>(params object[] constructorParameters)
        {
            OutputFriendly(typeof(TInitializer), constructorParameters, "\tand ");
            Initialize(typeof(TInitializer), constructorParameters);
            return this;
        }

        IExpectationFluent<TTarget> IInitializationFluent<TTarget>.When<TAction>(params object[] args)
        {
            RunInitializers();

            OutputFriendly(typeof(TAction), args, "when ");
            this.actionInstance = (TAction)Activator.CreateInstance(typeof(TAction), args);
            return this;
        }

        protected override void RunActionAsInitializer(IActionable action)
        {
            ExecuteActionWithoutExpectedExceptions(action);
        }

        #endregion

        #region Throw

        void IExceptionExpected.ThenThrow(Type exceptionType)
        {
            OutputFriendly(exceptionType, null, "then throw ");

            // We expect action to thrown a certain exception type
            ExecuteActionAndExpectException(this.actionInstance, exceptionType);
        }

        void IExceptionExpected.ThenThrow<TException>()
        {
            (this as IExceptionExpected).ThenThrow(typeof(TException));
        }

        #endregion

        #region With

        IInitializationFluent<TTarget> IInitializationFluent<TTarget>.WithContext(Action<dynamic> contextSetter)
        {
            this.SetContext(contextSetter);
            return this;
        }

        #endregion
    }

    public abstract class UnitTest<TTarget, TReturnType> : UnitTestBase<TTarget>, IInitializationFluent<TTarget, TReturnType>, IExpectationFluent<TTarget, TReturnType>, IFurtherExpectationFluent<TTarget, TReturnType>
    {
        #region Methods

        protected IInitializationFluent<TTarget, TReturnType> GivenThat<TInitializer>(params object[] constructorParameters)
                where TInitializer : IInitializerMarker
        {
            GivenThat(typeof(TInitializer), constructorParameters);
            return this;
        }

        IInitializationFluent<TTarget, TReturnType> IInitializationFluent<TTarget, TReturnType>.And<TInitializer>(params object[] constructorParameters)
        {
            Initialize(typeof(TInitializer), constructorParameters);
            return this;
        }

        IFurtherExpectationFluent<TTarget, TReturnType> IExpectationFluent<TTarget, TReturnType>.Then<TAssertion>(params object[] constructorParameters)
        {
            OutputFriendly(typeof(TAssertion), constructorParameters, "then ");
            this.returnValue = ExecuteActionWithoutExpectedExceptions<IActionable, TReturnType>(this.actionInstance);
            return Result<TAssertion>(constructorParameters);
        }

        IFurtherExpectationFluent<TTarget, TReturnType> Result<TAssertion>(params object[] constructorParameters)
        {
            var assertion = Activator.CreateInstance(typeof(TAssertion), constructorParameters) as IAssertion<TTarget, TReturnType>;
            assertion.Verify(this.target, this.returnValue);
            return this;
        }

        IFurtherExpectationFluent<TTarget, TReturnType> IFurtherExpectationFluent<TTarget, TReturnType>.And<TAssertion>(params object[] constructorParameters)
        {
            OutputFriendly(typeof(TAssertion), constructorParameters, "\tand ");
            return Result<TAssertion>(constructorParameters);
        }

        IExpectationFluent<TTarget, TReturnType> IInitializationFluent<TTarget, TReturnType>.When<TAction>(params object[] args)
        {
            RunInitializers();

            OutputFriendly(typeof(TAction), args, "when ");
            this.actionInstance = (TAction)Activator.CreateInstance(typeof(TAction), args);
            return this;
        }

        protected override void RunActionAsInitializer(IActionable action)
        {
            if (action == null) return;
            ExecuteActionWithoutExpectedExceptions<IActionable, TReturnType>(action);
            this.SetContext(string.Format("{0}ReturnValue", action.GetType().Name), returnValue);
        }

        #endregion

        #region Throw

        void IExceptionExpected.ThenThrow(Type exceptionType)
        {
            OutputFriendly(exceptionType, null, "then throw ");

            // We expect action to thrown a certain exception type
            this.returnValue = ExecuteActionAndExpectException<IActionable, TReturnType>(this.actionInstance, exceptionType);
        }

        void IExceptionExpected.ThenThrow<TException>()
        {
            (this as IExceptionExpected).ThenThrow(typeof(TException));
        }

        #endregion

        #region Fields

        private TReturnType returnValue;

        #endregion
    }
}
